﻿using System;
using System.Collections.Generic;

//using System.Text;
//using System.Threading.Tasks;
using Catfood.Shapefile;
using System.IO;
using System.IO.Compression;
using System.Drawing;
using System.Threading;
using System.Drawing.Imaging;
using System.Reflection;
using System.Linq;

namespace Genesis.ShapeImage
{ 
    public class Transformer
    {
       
        private int unexpectedError = 1;
        private int notFoundNeededFiles = 2;
        private int errorGetShapes = 3;
        private int buildImageError = 4;
        private int wrongShapeType = 5;
        private int wrongImageFormat = 6;
        private int errorConversionShapeCoordsToPictureCoords = 7;

        #region Public properties
        public int imageWidthInPixels { get; set; }
        public int pointSymbolRadiusPixels { get; set; }
        public string pathToPointImage { get; set; }
        public string linesColor { get; set; }
        public float linesWidth { get; set; }

        /// <summary>
        /// This property is set in ExtractZipFile method but it also could be set
        /// by calling program. In any case the directory where shape file is found must contain
        /// corresponding shx-and dbf files.
        /// </summary>
        public string pathToShapeFile { get; set; }


        #endregion

        #region Private properties
        /// <summary>
        /// This property is set in GetShapes method
        /// </summary>
        private Dictionary<int, MyShape> dictOutput { get; set; }
        #endregion





        public Transformer()
        {
            //default values for properties
            imageWidthInPixels = 2500;
            pointSymbolRadiusPixels = 25;
            string assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            pathToPointImage = Path.Combine(assemblyFolder, "ball.jpg");
          //  pathToPointImage = @"C:\Shapefiles\Images\ball.jpg" ;
                   
            linesColor = "Black";
            linesWidth = 3;
        }


        /// <summary>
        /// This is the main public method of this class
        /// Given zip file containing shape information
        /// (see https://en.wikipedia.org/wiki/Shapefile)
        ///  the method builds an image presenting the shapes in graphic form
        /// </summary>
        /// <param name="fullPathToZipFile">Input: full path to zip-file</param>
        /// <param name="outputDirectory">input: path to directory 
        /// where the image will be saved. Also a subdirectory will be created in this directory
        /// and the zip file will be unzipped into this subdirectory.
        /// </param>
        /// <param name="imageName">Input: name of the resulted image file</param>
        /// <param name="imageFormat">Input:format of the image (png, jpg and so on)</param>
        /// <param name="drawingBounds">Output:array of 4 doubles xmin,ymin, xmax,ymax
        /// where the first two are shape coordinates of left bottom corner of the bounding box of the shapes
        /// and the second two are shape coordinates of the right bottom corner of this box
        /// </param>
        /// <param name="errMessage"> Output: an exception description or empty</param>
        /// <returns>return code</returns>
        public int convertShapeToImage(string fullPathToZipFile, string outputDirectory, string imageName,
            string imageFormat,
             out double[] drawingBounds, out string errMessage)
        {
            int returnCode = 0;
            drawingBounds = new double[4];
            char[] sep = new char[1] { '\\' };
            try
            {
                string extractPath = string.Empty;
                 errMessage = string.Empty;
                returnCode = ExtractZipFile(fullPathToZipFile, outputDirectory, 
                    out extractPath, out errMessage);
                Thread.Sleep(1000);
                if (returnCode == 0)
                {
                    if (imageName == string.Empty)
                    {
                        string[] arr = extractPath.Split(sep);
                        imageName = arr[arr.Length - 1];
                    }
                   returnCode=  BuildImage( outputDirectory,imageName, imageFormat,
                       out  drawingBounds ,out  errMessage);
                }
            }
            catch (Exception ex)
            {

                returnCode = unexpectedError;
                errMessage = ex.Message;
            }
            return returnCode;
        }

        /// <summary>
        /// For external use only
        /// </summary>
        /// <returns></returns>
        public MyRectangle GetBoundingBox()
        {
            MyRectangle box;
            try
            {
                Shapefile shapefile = new Shapefile(pathToShapeFile);
                box = new MyRectangle(shapefile.BoundingBox.Left,                    
                    shapefile.BoundingBox.Bottom,
                    shapefile.BoundingBox.Right,
                    shapefile.BoundingBox.Top                     
                   );
            }
            catch (Exception ex)
            {

                throw ex;
            }
            return box;
        }
        /// <summary>
        /// Given pathToShapeFile builds dictionary of shapes
        /// </summary>
        /// <param name="boundingBox">Output: bounding box of all shapes</param>
        /// <param name="errMessage">Output: error message</param>
        /// <returns>return code</returns>
        public int GetShapes(out MyRectangle boundingBox,out string errMessage)
        {
            int returnCode = 0;
            boundingBox = new MyRectangle();
            errMessage = string.Empty;
            try
            {
                Shapefile shapefile = new Shapefile(pathToShapeFile);
                boundingBox = new MyRectangle(shapefile.BoundingBox.Left,
                    shapefile.BoundingBox.Bottom,
                    shapefile.BoundingBox.Right,
                    shapefile.BoundingBox.Top
                   );
                int RecordNumber = 0;
                dictOutput = new Dictionary<int, MyShape>();
                MyShape currentShape;                
               
                MyShapePoint pnt;
                MyShapePoint[] currentPart;
                List<MyShapePoint> lstCurrentPartPoints = null;
                int key = 0;
                foreach (Shape shape in shapefile)
                {
                    RecordNumber = shape.RecordNumber;
                    
                    // cast shape based on the type
                    currentShape = new MyShape();
                    currentShape.shapeParts = new List<MyShapePoint[]>();
                    switch (shape.Type)
                    {
                        case ShapeType.Point:
                        case ShapeType.PointZ:
                        case ShapeType.PointM:
                            key++;
                            // a point is just a single x/y point                           
                            ShapePoint shapePoint = shape as ShapePoint;                           
                            pnt = new MyShapePoint( shapePoint.Point.X, shapePoint.Point.Y );                            
                            
                            currentShape.shapeType = MyShapeType.Point;
                            currentShape.shapeParts.Add(new MyShapePoint[1] { pnt });
                            dictOutput[key] = currentShape;
                            break;

                        case ShapeType.Polygon:
                        case ShapeType.PolygonZ:
                        case ShapeType.PolygonM:                       
                            key++;                            
                                currentShape.shapeType = MyShapeType.Polygon;
                           
                            // a polygon contains one or more parts - each part is a list of points which
                            // are clockwise for boundaries and anti-clockwise for holes 
                            // see http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf
                            ShapePolygon shapePolygon = shape as ShapePolygon;
                            foreach (PointD[] part in shapePolygon.Parts)
                            {

                                lstCurrentPartPoints = new List<MyShapePoint>();
                                foreach (PointD point in part)
                                {                                    
                                    pnt = new MyShapePoint ( point.X, point.Y );
                                    lstCurrentPartPoints.Add(pnt);
                                }
                                currentPart = lstCurrentPartPoints.ToArray();
                                currentShape.shapeParts.Add(currentPart);
                            }
                            dictOutput[key] = currentShape;
                            break;
                        case ShapeType.PolyLine:
                        case ShapeType.PolyLineZ:
                        case ShapeType.PolyLineM:
                            key++;   
                                currentShape.shapeType = MyShapeType.PolyLine;
                                ShapePolyLine shapePolyline = shape as ShapePolyLine;
                                foreach (PointD[] part in shapePolyline.Parts)
                            {

                                lstCurrentPartPoints = new List<MyShapePoint>();
                                foreach (PointD point in part)
                                {                                    
                                    pnt = new MyShapePoint ( point.X, point.Y );
                                    lstCurrentPartPoints.Add(pnt);
                                }
                                currentPart = lstCurrentPartPoints.ToArray();
                                currentShape.shapeParts.Add(currentPart);
                            }
                            dictOutput[key] = currentShape;
                            break;
                        default:
                            returnCode = wrongShapeType;
                            break;
                    }
                    if (returnCode != 0) break;
                   
                }
            }
            catch (Exception ex)
            {

                returnCode = unexpectedError;
                errMessage = ex.Message;
            }
            return returnCode;
        }

        /// <summary>
        /// This method unzips zipfile, containing shp-file, shx-file and dbf-file
        /// and save results in new subdirectory  of given directory      
        /// </summary>
        /// <param name="pathToZipFile">Input: full path to zip-file</param>
        /// <param name="outputDirectory">input: path to directory 
        /// where new subdirectory will be created
        /// </param>      
        /// <param name="extractPath">output:
        /// path to new subdirectory containing extracted files
        /// It is concatenation of outputDirectory and name of the zip file without extension
        /// </param>
        /// <param name="errMessage">output: message of exception if it was fired</param>
        /// <returns>return code:0 if the method succeeded,
        /// value of unexpectedError variable (1) if there was an exception,
        /// value of notFoundNeededFiles (2) if content of the zip-file is not valid:
        /// content is valid if the file contains exactly one file with extensions shp,shx,dbf
        ///  (and may be some  files with other extensions)
        /// </returns>
        /// If the method succeeded then it assignes path to the shp-file to property pathToShapeFile
        public int ExtractZipFile(string pathToZipFile, string outputDirectory,  
            out string extractPath,out string errMessage)
        {
            int returnCode = 0;
           
            extractPath =string.Empty;
            errMessage = string.Empty;
            try
            {
               
                //define path to directory where extracted files will be saved               
                extractPath = outputDirectory + "\\" +
                    Path.GetFileNameWithoutExtension(pathToZipFile);
                //extract:
                ZipFile.ExtractToDirectory(pathToZipFile, extractPath);
                //check that the result directory contains shp,shx, dbf-files:             

                string[] shpfiles = Directory.GetFiles(extractPath,  "*.shp");
                string[] shxfiles = Directory.GetFiles(extractPath,  "*.shx");
                string[] dbffiles = Directory.GetFiles(extractPath,  "*.dbf");
                if (shpfiles.Length != 1 || shxfiles.Length != 1 || dbffiles.Length != 1)
                    returnCode = notFoundNeededFiles;
                else                  
                    this.pathToShapeFile = shpfiles[0];
            }
            catch (Exception ex)
            {               
                returnCode = unexpectedError;
                errMessage = ex.Message;
            }
            return returnCode;
        }
        /// <summary>
        ///  Given pathToShapeFile this method builds an image and saves it in output directory
        /// </summary>
        /// <param name="outputDirectory"></param>
        /// <param name="imageName"></param>
        /// <param name="extension"></param>
        /// <param name="drawingBounds"></param>
        /// <param name="errMessage"></param>
        /// <returns></returns>
        public int BuildImage(string outputDirectory, string imageName, string extension,
            out double[] drawingBounds,
            out string errMessage)
        {
            int returnCode = 0;
            errMessage = string.Empty;
            drawingBounds = new double[4];
            MyRectangle boundingBox = new MyRectangle();
            try
            {
               
                try
                {
                    //this method builds this.dictOutput
                    returnCode = GetShapes(out boundingBox,out errMessage);
                    if (returnCode != 0)
                        return returnCode;
                }
                catch (Exception ex1)
                {
                    returnCode = errorGetShapes;
                    errMessage = ex1.Message;
                    return returnCode;
                }

                Dictionary<int, MyShape> dict = this.dictOutput;

                drawingBounds[0] = boundingBox.Left;
                drawingBounds[1] = boundingBox.Bottom;
                drawingBounds[2] = boundingBox.Right;
                drawingBounds[3] = boundingBox.Top;

                double imageWidthInShapeUnits = boundingBox.Right - boundingBox.Left;
                double imageHeightInShapeUnits = boundingBox.Top - boundingBox.Bottom;
                double widthToHeightRelaton = imageWidthInShapeUnits / imageHeightInShapeUnits;

                int imageHeightInPixels = (int)Math.Round(imageWidthInPixels / widthToHeightRelaton, 0);

                Color lineColor = Color.FromName(this.linesColor);
                Pen linePen = new Pen(lineColor, linesWidth);

                Bitmap bitmap = new Bitmap(imageWidthInPixels, imageHeightInPixels);

                System.Drawing.Image OutputImage = bitmap;
                
                int[] pointCoords = null;

                using (var graphics = Graphics.FromImage(OutputImage))
                {
                    graphics.Clear(Color.White);
                    foreach (int key in dict.Keys)
                    {
                        MyShape shape = dict[key];
                        List<MyShapePoint[]> shapeParts = shape.shapeParts;
                        MyShapeType shapeType = shape.shapeType;
                        foreach (MyShapePoint[] pntArray in shapeParts)
                        {
                            switch (shapeType)
                            {
                                case MyShapeType.Point:
                               
                                    MyShapePoint pnt = pntArray[0];
                                    Image img = Image.FromFile(pathToPointImage);
                                    returnCode = ConvertShapeCoordsToPictureCoords(boundingBox, pnt, imageWidthInPixels, imageHeightInPixels,
                                         out pointCoords, out errMessage);
                                    if (returnCode == 0)
                                    {
                                        Point pnt1 = new Point(pointCoords[0], pointCoords[1]);
                                        graphics.DrawImage(img, pnt1);
                                    }
                                    break;
                                case MyShapeType.PolyLine:
                               
                                    List<int[]> lineImageCoords = new List<int[]>();
                                    for (int i = 0; i < pntArray.Length; i++)
                                    {
                                        returnCode = ConvertShapeCoordsToPictureCoords(boundingBox, pntArray[i], imageWidthInPixels, imageHeightInPixels,
                                            out pointCoords, out errMessage);
                                        if (returnCode == 0)
                                        {
                                            lineImageCoords.Add(pointCoords);
                                        }
                                        else
                                        {
                                            break;
                                        }
                                    }
                                    if (returnCode == 0)
                                    {
                                        for (int i = 0; i < pntArray.Length - 1; i++)
                                        {
                                            int x1 = lineImageCoords[i][0];
                                            int y1 = lineImageCoords[i][1];
                                            int x2 = lineImageCoords[i + 1][0];
                                            int y2 = lineImageCoords[i + 1][1];

                                            //now run over lineImageCoords and add to bitmap
                                            graphics.DrawLine(linePen, x1, y1, x2, y2);
                                        }
                                    }
                                    break;
                                case MyShapeType.Polygon:
                                    List<int[]> polygonImageCoords = new List<int[]>();
                                    for (int i = 0; i < pntArray.Length; i++)
                                    {
                                        returnCode = ConvertShapeCoordsToPictureCoords(boundingBox, pntArray[i], imageWidthInPixels, imageHeightInPixels,
                                         out pointCoords, out errMessage);
                                        if (returnCode == 0)
                                            polygonImageCoords.Add(pointCoords);
                                        else
                                            break;
                                    }
                                    if (returnCode == 0)
                                    {
                                        Point[] points = new Point[pntArray.Length];
                                        for (int i = 0; i < pntArray.Length; i++)
                                        {
                                            points[i].X = polygonImageCoords[i][0];
                                            points[i].Y = polygonImageCoords[i][1];

                                        }
                                        graphics.DrawPolygon(linePen, points);
                                    }
                                    break;
                                default:
                                    returnCode = wrongShapeType;
                                    break;
                            }
                        }
                    } //end cycle by key


                    imageName += "." + extension;
                    ImageFormat frm = GetImageFormat(extension);
                    if (frm != null)
                        OutputImage.Save(outputDirectory + "\\" + imageName, frm);
                    else
                    {
                        returnCode = wrongImageFormat;
                    }
                }
            }
            catch (Exception ex)
            {
                returnCode = buildImageError; ;
                errMessage = ex.Message;
            }
            return returnCode;
        }

        private  int ConvertShapeCoordsToPictureCoords(MyRectangle boundingBox,MyShapePoint shape_coords,
        int PictureWidth, int PictureHeight,out int[] pictureCoords,out string errMessage)
        {
            int returnCode = 0;
            errMessage = string.Empty;
            pictureCoords = new int[2];
            try
            {
               
                double XShapeMin = boundingBox.Left - pointSymbolRadiusPixels;
                double YShapeMin = boundingBox.Bottom - pointSymbolRadiusPixels;
                double XShapeMax = boundingBox.Right + pointSymbolRadiusPixels;
                double YShapeMax = boundingBox.Top + pointSymbolRadiusPixels;

                double shapeWidth = XShapeMax - XShapeMin;
                double shapeHeight = YShapeMax - YShapeMin;
                double x = shape_coords.X;
                double y = shape_coords.Y;

                pictureCoords[0] = Convert.ToInt32(PictureWidth * (x - XShapeMin) / shapeWidth);
                pictureCoords[1] = Convert.ToInt32(PictureHeight * (1 - (y - YShapeMin) / shapeHeight));
            }
            catch (Exception ex)
            {

                returnCode= errorConversionShapeCoordsToPictureCoords;
                errMessage = ex.Message;
            }
           

            return returnCode;

        }

        private  ImageFormat GetImageFormat(string extension)
        {
            ImageFormat result = null;
            PropertyInfo prop = 
                typeof(ImageFormat).GetProperties().Where(p => p.Name.Equals(extension, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
            if (prop != null)
            {
                result = prop.GetValue(prop) as ImageFormat;
            }
            return result;
        }

    }
}
